Skip to content

Risograph-editorial redesign + security/a11y/perf/SEO hardening#19

Open
rahmanef63 wants to merge 2 commits into
IndopenSource:mainfrom
rahmanef63:feat/riso-editorial-redesign
Open

Risograph-editorial redesign + security/a11y/perf/SEO hardening#19
rahmanef63 wants to merge 2 commits into
IndopenSource:mainfrom
rahmanef63:feat/riso-editorial-redesign

Conversation

@rahmanef63

@rahmanef63 rahmanef63 commented Jun 15, 2026

Copy link
Copy Markdown

Ringkasan

PR ini membawa dua hal ke situs hub indopensource.org, di atas base main saat ini:

  1. Pengerasan engineering — keamanan, aksesibilitas, performa, SEO, kebenaran data. Perbaikan konkret, bisa langsung di-merge.
  2. Redesain UI/UX "risograph editorial" — arah visual baru yang khas (bukan template generik), self-hosted, aksesibel.

Semua perubahan menjaga astro check bersih, 16 unit test baru hijau, build 57 halaman, dan 0 inline style (CSP style-src 'self' tetap utuh). Dibuka untuk diskusi — silakan ambil sebagian atau seluruhnya.

Catatan: ini perubahan besar. Dengan senang hati saya pecah jadi beberapa PR yang lebih kecil kalau itu lebih enak direview — tinggal bilang.


1. Pengerasan engineering

Keamanan

  • Stored XSS di blog ditutup. marked.parse(post.content) sebelumnya masuk set:html tanpa sanitizer (marked v18 tidak punya sanitizer bawaan). Kini lewat allowlist sanitize-html terpusat (src/lib/content.tsrenderArticle()), dipakai juga untuk README project yang sekarang dirender saat build (bukan fetch klien).
  • CSP via <meta>: script-src 'self' + hash JSON-LD (tanpa unsafe-inline), style-src 'self', frame-ancestors 'none', object-src 'none'; rel="noopener noreferrer" dipaksa di semua link hasil sanitasi.

Konten & kebenaran data

  • Draft gating: isPublished() dipakai di getStaticPaths, pemilihan featured, dan sitemap — post status: draft tidak menghasilkan halaman dan tidak masuk sitemap.
  • Atribusi penulis diambil dari frontmatter authors[], bukan committer git.
  • parseFrontmatter diperkuat (CRLF, komentar inline, single/double quote, duplicate key, tanpa trailing newline), sort deterministik, resolusi default-branch repo via API (tidak hardcode main), total stars jujur (entry yang gagal di-sync ditandai & dikecualikan dari agregat).
  • projectSlug anti-collision (suffix FNV-1a) + hapus fungsi inverse yang mati.

SEO & aksesibilitas

  • og:image / twitter:image + og:image:alt per-halaman; JSON-LD (WebSite / BlogPosting / SoftwareSourceCode); sitemap /sitemap.xml dengan lastmod dari Astro.site; trailingSlash: 'always'.
  • Skip-to-content link, :focus-visible, satu <h1> per halaman, kontras WCAG AA diverifikasi numerik, prefers-reduced-motion.

Performa

  • Optimasi gambar via astro:assets <Picture> (AVIF/WebP): hero 60KB → ~8.5KB, logo 18KB → 737B; width/height di semua <img> (CLS ~0); preload LCP hero. README dirender saat build → tidak ada lagi fetch GitHub API per kunjungan.

Tooling

  • 16 unit test (node --test) untuk slug, isPublished, sanitizer/XSS, dan parser frontmatter; tambah .env.example.

2. Redesain "Risograph Editorial"

Arah visual: zine cetak risograph Indonesia — hangat, bertekstur, percaya diri. Bukan SaaS gradient.

  • Tipografi (self-hosted via @fontsource, tanpa CDN): Bricolage Grotesque (display), Newsreader (serif body), Spline Sans Mono (slug/stat/metadata).
  • Palet spot-ink riso terdisiplin (cream / teal / vermilion / sun); semua token teks AA-verified pada bidangnya.
  • Sistem: sticker, rubber-stamp, indeks seksi oversized, halftone, hard offset shadow, rule tinta tebal, panel tonal (ink / vermilion / sun), grain dua frekuensi.
  • Motion: reveal scroll-aware (IntersectionObserver sebagai module ter-bundle → CSP-safe), aman terhadap reduced-motion.
  • Hero: foto dijadikan lapisan tinta duotone (grayscale + mix-blend + halftone) sambil tetap memakai <Picture> AVIF teroptimasi.

Tangkapan layar

Home Projects
Home Projects
Blog Falsafah
Blog Falsafah
Contact Forum
Contact Forum

Catatan scope

  • Sengaja TIDAK disertakan (spesifik hosting/operasional, bukan untuk upstream): perubahan .github/workflows/*, vercel.json, dan tweak README.md / SECURITY.md.
  • astro.config.mjs site tetap https://indopensource.org.
  • Gambar di docs/redesign/ hanya ilustrasi untuk review — silakan dihapus sebelum merge bila lebih suka attachment.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Redesigned homepage with new hero imagery and editorial layout
    • Enhanced blog with curated featured posts and improved metadata rendering
    • Project showcase now displays README previews
    • Restructured contact page with organized channel cards
    • New visual design refresh across the site
  • Improvements

    • Better SEO with updated robots/sitemap behavior and richer structured data
    • Faster, more consistent media loading and canonical routing
  • Security

    • Hardened article rendering with sanitization and stricter content security policies
  • Bug Fixes

    • More reliable post/project synchronization, ordering, and fallback handling
  • Tests

    • Added unit coverage for URL handling, publishing rules, and rendering

Full breakdown in the PR description. Highlights:
- security: blog stored-XSS closed via a shared sanitize-html allowlist
  (src/lib/content.ts renderArticle, reused for build-time README render);
  CSP with hashed JSON-LD and no unsafe-inline; rel=noopener on sanitized links
- content: draft gating (isPublished) across pages/featured/sitemap; author from
  frontmatter authors[]; hardened parseFrontmatter; collision-safe projectSlug;
  default-branch resolution; honest sync-failure star totals
- seo/a11y: per-page og:image + JSON-LD (WebSite/BlogPosting/SoftwareSourceCode);
  /sitemap.xml with lastmod; trailingSlash always; skip-link, focus-visible,
  single h1, WCAG-AA contrast verified, prefers-reduced-motion
- perf: astro:assets <Picture> AVIF/WebP (hero 60KB->~8.5KB, logo 18KB->737B),
  width/height on all <img> (CLS ~0), hero LCP preload
- design: risograph-editorial system — self-hosted variable fonts (Bricolage /
  Newsreader / Spline Sans Mono), disciplined riso spot palette, sticker/stamp/
  halftone/grain/hard-shadow utilities, scroll-aware IntersectionObserver reveal
  (bundled module, CSP-safe), duotone riso hero keeping the optimized <Picture>
- tooling: 16 node --test unit tests (slug/isPublished/sanitizer-XSS/parser); .env.example

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

return `${base}-${shortHash(fullName)}`;

P2 Badge Preserve existing project detail slugs

Because this helper feeds both getStaticPaths and every project-card/detail URL, appending the hash to every project changes all existing public project URLs, e.g. /projects/opensid--opensid/ becomes /projects/opensid--opensid-db3yyo/. Without redirects, bookmarked and search-indexed detail pages for all non-colliding repos will 404 after deploy; reserve the suffix for actual collisions or add redirects from the previous slug shape.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/sync-projects.mjs Outdated
const resolve = (value, base) => {
if (!value || value.startsWith('#') || /^(https?:|mailto:)/i.test(value)) return value;
try {
const url = new URL(value, base);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve README root-relative paths within the repo

For README links or images that start with /, this now resolves against the host root, so /docs/setup.md becomes https://github.com/docs/setup.md (or https://raw.githubusercontent.com/docs/setup.md) instead of staying under the synced repository. The previous client-side sanitizer had repo-root handling for this case; after pre-rendering, any project README that uses root-relative docs/assets links will render broken links/images on the detail page.

Useful? React with 👍 / 👎.

with a JSON string is safe here: it is `JSON.stringify` output, not
user-controlled markup.
-->
<script type="application/ld+json" is:inline set:html={jsonLdString} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape JSON-LD before injecting it

When synced data used in jsonLd (such as a GitHub repo description or blog frontmatter title/description) contains </script>, JSON.stringify does not make it safe for a raw script context, so the browser can terminate this JSON-LD block and parse attacker-controlled markup in the page head. CSP should block a follow-on inline script, but non-script markup such as a meta refresh can still be injected and the structured data breaks; escape < (for example to \u003c) before set:html.

Useful? React with 👍 / 👎.

@wauputr4

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 969052a0-84e7-40d3-b499-893b318624b1

📥 Commits

Reviewing files that changed from the base of the PR and between 6044270 and 0896271.

📒 Files selected for processing (6)
  • scripts/sync-blog-posts.mjs
  • scripts/sync-projects.mjs
  • src/layouts/BaseLayout.astro
  • src/lib/urls.ts
  • src/pages/blog/[slug].astro
  • test/lib.test.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/lib.test.mjs
  • scripts/sync-projects.mjs
  • scripts/sync-blog-posts.mjs
  • src/pages/blog/[slug].astro
  • src/layouts/BaseLayout.astro

📝 Walkthrough

Walkthrough

This PR introduces a riso/zine visual system, build-time sanitized content rendering, dynamic repository synchronization, stronger layout metadata and CSP handling, replacement sitemap routes, redesigned page layouts, and unit tests for shared helpers.

Changes

Riso design system and content pipeline

Layer / File(s) Summary
Content contracts, sanitization, and shared assets
package.json, src/lib/content.ts, src/lib/hero.ts, src/lib/urls.ts, test/lib.test.mjs
Adds typed content helpers, Markdown sanitization, safe URL handling, shared hero constants, package scripts/dependencies, and tests.
Repository synchronization and post metadata
scripts/sync-blog-posts.mjs, scripts/sync-projects.mjs
Adds frontmatter parsing, dynamic default branches, README pre-rendering, editorial and modification dates, author precedence, deterministic sorting, SEO descriptions, and failed-sync flags.
Layout security, routing, and sitemap infrastructure
.env.example, astro.config.mjs, src/layouts/BaseLayout.astro, src/pages/robots.txt.ts, src/pages/sitemap.xml.ts, src/pages/sitemap-index.xml.ts
Adds typed metadata, CSP hashing, preload and accessibility behavior, dynamic robots/sitemap generation, and Astro build settings.
Riso CSS and decorative motion system
src/styles/global.css
Adds self-hosted fonts, riso tokens/utilities, prose styling, reveal behavior, decorative motion, and reduced-motion handling.
Shared components and homepage hero
src/components/*
Restyles shared components, navigation, footer, timeline, decorative SVGs, and the image-driven hero.
Blog and project page integrations
src/pages/blog*, src/pages/projects*
Applies publication filtering, sanitized article rendering, project README HTML, page metadata, structured data, and updated directory/detail layouts.
Editorial page layouts
src/pages/index.astro, src/pages/contact.astro, src/pages/falsafah.astro, src/pages/forum.astro
Adds numbered editorial sections, data-driven cards, pull quotes, calls to action, and a redesigned development note.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: wauputr4

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main changes: a risograph-style redesign plus security, accessibility, performance, and SEO hardening.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
package.json (1)

6-14: ⚠️ Potential issue | 🟡 Minor

Declare the Node runtime contract in package.json.

The PR adds .mjs files that import .ts modules directly (e.g., scripts/sync-projects.mjs imports ../src/lib/content.ts, and test/lib.test.mjs imports multiple .ts files). While CI workflows already pin node-version: 22, package.json lacks an engines.node declaration. This leaves local developers without explicit version guidance and breaks the standard contract that tools and IDEs expect.

Add "engines": { "node": ">=22" } to package.json to match the enforced CI version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 6 - 14, The package.json file is missing an
engines declaration to specify the Node.js version requirement. Add an "engines"
field to the root level of package.json (at the same level as "scripts") with
the value "node": ">=22" to explicitly document that the project requires
Node.js version 22 or higher, matching the CI workflow requirements and
providing clear guidance to local developers.
src/pages/projects.astro (1)

66-66: ⚠️ Potential issue | 🟡 Minor

Filter ProjectsDirectory to refreshedProjects or visually mark syncFailed entries.

The directory currently displays all projects including syncFailed entries, which show placeholder zero stars/forks without any visual indicator of failure. This contradicts the stated goal (CC-10) of showing "honest" totals—users will see projects with 0 stars and no explanation. The unavailability message (line 45) only explains the aggregate exclusion, not why individual cards display zeros. Either pass refreshedProjects to ProjectsDirectory on line 66, or add a "Sync Failed" status badge to ProjectCard (line 11 in ProjectCard.astro) to surface the issue explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/projects.astro` at line 66, The ProjectsDirectory component is
currently displaying all projects including those with syncFailed status, which
show placeholder zero stars/forks without any visual indicator of failure,
contradicting the goal of showing honest totals. To fix this, either pass
refreshedProjects instead of projects to the ProjectsDirectory component to
exclude failed syncs entirely, or add a "Sync Failed" status badge to the
ProjectCard component to visually mark entries that failed to sync, making it
clear to users why those cards display zero metrics.
src/components/ProjectsDirectory.astro (1)

96-182: ⚠️ Potential issue | 🔴 Critical

Move script to external bundled file or add nonce/hash support to CSP policy.

This inline <script> will be blocked by the BaseLayout CSP policy (script-src 'self'). Astro 6.x inlines plain <script> tags in the HTML output by default, which violates the strict script-source constraint. The filtering, sorting, and pagination will fail at runtime.

Options:

  • Move the script to an external module and import it, or
  • Update the CSP policy to include script-src nonce/hash directives if inline scripts are required.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ProjectsDirectory.astro` around lines 96 - 182, The inline
script tag in ProjectsDirectory.astro containing the project filtering, sorting,
and pagination logic is blocked by the BaseLayout's CSP policy which restricts
scripts to 'self'. Extract the JavaScript code from the inline script block into
a separate external file (e.g., projectsDirectory.js or similar), then import
and execute it from the component instead of using an inline script tag. This
will allow the bundler to handle it properly and comply with the strict CSP
policy. Alternatively, if inline scripts must be kept, update the CSP policy in
BaseLayout to support nonce or hash directives for script-src.
🧹 Nitpick comments (3)
astro.config.mjs (1)

16-23: 💤 Low value

Misleading comment: inlineStylesheets does not control script inlining.

The comment mentions preventing inline <script> for CSP compliance, but build.inlineStylesheets only controls stylesheet emission, not scripts. Astro's script bundling is controlled separately—module scripts (like the scroll-reveal script) are already emitted as external /_astro/*.js files by default in Astro v5+.

The setting is still correct for preventing inline <style> blocks that would violate style-src 'self', but the comment should be updated to reflect this.

📝 Suggested comment fix
   build: {
-    // Never inline the scroll-reveal `<script>` (or any other) into the HTML.
-    // A strict `script-src 'self'` CSP (no nonce, no 'unsafe-inline') would block
-    // an inline code-bearing <script>; forcing Astro to emit it as an external
-    // /_astro/*.js entry referenced via `<script src>` keeps it covered by 'self'.
-    // (SEC-02 / MOTION)
+    // Never inline stylesheets into the HTML. A strict `style-src 'self'` CSP
+    // (no 'unsafe-inline') would block inline <style> blocks; forcing Astro to
+    // emit all CSS as external /_astro/*.css files keeps them covered by 'self'.
+    // (SEC-02)
     inlineStylesheets: 'never'
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@astro.config.mjs` around lines 16 - 23, The comment in the build
configuration block for astro.config.mjs is misleading because it describes
inlineStylesheets as controlling script inlining for CSP compliance, when this
setting actually only controls stylesheet emission (preventing inline <style>
blocks). Update the comment to accurately reflect that inlineStylesheets:
'never' prevents inline stylesheets that would violate style-src 'self', and
clarify that script bundling is handled separately by Astro and already emits
module scripts as external files by default in Astro v5+.
src/layouts/BaseLayout.astro (1)

118-121: 💤 Low value

Hardcoded type="image/avif" assumes all preloaded heroes are AVIF.

If heroPreloadHref ever points to a WebP, JPEG, or other format, the browser may ignore or mishandle the preload due to the mismatched MIME type. Currently the homepage generates AVIF via getImage, so this works, but the coupling is implicit.

Consider accepting the image type as an additional prop or omitting the type attribute entirely (browsers will sniff the format).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/layouts/BaseLayout.astro` around lines 118 - 121, The preload link
element in BaseLayout.astro hardcodes type="image/avif" regardless of the actual
format of heroPreloadHref, which will cause browsers to ignore or mishandle
preloads for non-AVIF images. Fix this by either accepting a heroPreloadType
prop and using it dynamically in the type attribute of the link element, or
removing the type attribute entirely from the preload link tag and allowing the
browser to sniff the image format automatically. The simpler approach is to omit
the type attribute since browsers can detect the format from the file extension
or content.
src/pages/blog/[slug].astro (1)

35-41: ⚡ Quick win

Use an absolute URL for BlogPosting.image in JSON-LD.

thumbnailUrl can be relative, which weakens structured-data portability across crawlers and validators. Prefer an absolute URL in the JSON-LD payload.

♻️ Proposed refinement
 const thumbnail = post.thumbnail || '/brand/indopensource-hero.jpg';
 const thumbnailUrl = /^https?:\/\//.test(thumbnail) ? thumbnail : withBase(thumbnail);
+const siteOrigin = Astro.site ?? new URL('https://indopensource.org');
+const thumbnailAbsoluteUrl = new URL(thumbnailUrl, siteOrigin).toString();
@@
-  image: thumbnailUrl,
+  image: thumbnailAbsoluteUrl,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/blog/`[slug].astro around lines 35 - 41, The jsonLd object in the
BlogPosting schema uses thumbnailUrl which may be a relative URL, but structured
data requires absolute URLs for the image field to ensure proper portability
across crawlers and validators. Modify the jsonLd object to ensure the image
property receives an absolute URL by converting thumbnailUrl to a fully
qualified URL (including the domain) before assigning it to the image field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/sync-blog-posts.mjs`:
- Around line 87-97: The YAML parsing logic in the array item handling is
incorrectly coercing empty scalar keys into arrays, which corrupts post fields.
When an array item contains a key-value pair (like `- key: value`), if the value
portion is empty (after the colon), the code should not treat it as a scalar
field but rather handle it appropriately to prevent field type corruption.
Modify the condition around the key-value pair parsing to check whether
`rest.join(':')` produces an actual value after trimming; if it results in an
empty string, either skip that item, treat it as a null scalar, or handle it in
a way that prevents initializing array fields that should remain scalars. This
same pattern correction should be applied at line 111-114 as well.
- Around line 250-263: The regex pattern in the date validation check accepts
timezone-less datetimes (e.g., "2023-06-14T14:30:00") which parse using local
machine timezone, breaking deterministic sorting. The timezone indicator at the
end of the regex pattern is currently optional due to the trailing question
mark. Modify the regex to require the timezone indicator (Z or UTC offset)
whenever a time component is present, or alternatively, restrict the validator
to only accept plain dates in YYYY-MM-DD format without time components. This
ensures Date.parse always interprets dates consistently across all machines
regardless of local timezone settings.

In `@src/components/SiteHeader.astro`:
- Around line 63-65: The isCurrent check at lines 63-65 in SiteHeader.astro uses
strict equality which fails to mark parent nav items as current when viewing
detail pages like /blog/my-post/ since they don't exactly match /blog/.
Additionally, the same issue exists at lines 98-100. Replace the strict equality
comparison with section-aware matching that checks if the current path starts
with the href value, ensuring parent navigation items are correctly marked as
current for both overview pages and their dynamic route detail pages. Apply this
fix to both locations where isCurrent is evaluated.

In `@src/pages/blog/`[slug].astro:
- Around line 6-7: The post.author.url is being inserted directly into anchor
href attributes without validation, which creates a security vulnerability where
malicious JavaScript URLs could be executed. Validate the post.author.url before
rendering it into any href attribute by implementing a URL validation function
that only allows safe protocols (http, https, mailto) and rejects potentially
dangerous ones like javascript. Apply this validation wherever post.author.url
is used in href attributes throughout the file, ensuring that unsafe URLs are
either sanitized or replaced with empty strings or fallback values.

In `@src/pages/index.astro`:
- Around line 14-15: The topProjects variable is being created by slicing
directly from the full projects array, which includes entries with syncFailed
status. These failed sync entries should be excluded from the featured rail on
the homepage. Filter the projects array to exclude any entries where syncFailed
is true before slicing to create topProjects, ensuring that only successfully
synced repositories appear in the featured cards.

In `@src/styles/global.css`:
- Around line 178-186: In the `.sr-only` class definition, the deprecated `clip`
property needs to be replaced. Remove or replace the line containing `clip:
rect(0, 0, 0, 0);` with the modern `clip-path: inset(50%);` property. If legacy
browser support is required, you may keep the deprecated `clip` property as a
fallback on a separate line before the `clip-path` property, but the modern
property should be the primary rule.
- Line 16: The stylelint configuration does not recognize Tailwind v4's `@theme`
directive, causing CI failures for valid CSS. Update your stylelint
configuration file (typically `.stylelintrc.js`, `.stylelintrc.json`, or
similar) to allow Tailwind's custom at-rules by either installing and extending
a Tailwind-specific stylelint config package like
`@dreamsicle.io/stylelint-config-tailwindcss`, or manually modify the
`at-rule-no-unknown` rule in your existing stylelint config to add an exceptions
list that includes Tailwind's custom at-rules (such as `theme`, `layer`,
`apply`, etc.).

---

Outside diff comments:
In `@package.json`:
- Around line 6-14: The package.json file is missing an engines declaration to
specify the Node.js version requirement. Add an "engines" field to the root
level of package.json (at the same level as "scripts") with the value "node":
">=22" to explicitly document that the project requires Node.js version 22 or
higher, matching the CI workflow requirements and providing clear guidance to
local developers.

In `@src/components/ProjectsDirectory.astro`:
- Around line 96-182: The inline script tag in ProjectsDirectory.astro
containing the project filtering, sorting, and pagination logic is blocked by
the BaseLayout's CSP policy which restricts scripts to 'self'. Extract the
JavaScript code from the inline script block into a separate external file
(e.g., projectsDirectory.js or similar), then import and execute it from the
component instead of using an inline script tag. This will allow the bundler to
handle it properly and comply with the strict CSP policy. Alternatively, if
inline scripts must be kept, update the CSP policy in BaseLayout to support
nonce or hash directives for script-src.

In `@src/pages/projects.astro`:
- Line 66: The ProjectsDirectory component is currently displaying all projects
including those with syncFailed status, which show placeholder zero stars/forks
without any visual indicator of failure, contradicting the goal of showing
honest totals. To fix this, either pass refreshedProjects instead of projects to
the ProjectsDirectory component to exclude failed syncs entirely, or add a "Sync
Failed" status badge to the ProjectCard component to visually mark entries that
failed to sync, making it clear to users why those cards display zero metrics.

---

Nitpick comments:
In `@astro.config.mjs`:
- Around line 16-23: The comment in the build configuration block for
astro.config.mjs is misleading because it describes inlineStylesheets as
controlling script inlining for CSP compliance, when this setting actually only
controls stylesheet emission (preventing inline <style> blocks). Update the
comment to accurately reflect that inlineStylesheets: 'never' prevents inline
stylesheets that would violate style-src 'self', and clarify that script
bundling is handled separately by Astro and already emits module scripts as
external files by default in Astro v5+.

In `@src/layouts/BaseLayout.astro`:
- Around line 118-121: The preload link element in BaseLayout.astro hardcodes
type="image/avif" regardless of the actual format of heroPreloadHref, which will
cause browsers to ignore or mishandle preloads for non-AVIF images. Fix this by
either accepting a heroPreloadType prop and using it dynamically in the type
attribute of the link element, or removing the type attribute entirely from the
preload link tag and allowing the browser to sniff the image format
automatically. The simpler approach is to omit the type attribute since browsers
can detect the format from the file extension or content.

In `@src/pages/blog/`[slug].astro:
- Around line 35-41: The jsonLd object in the BlogPosting schema uses
thumbnailUrl which may be a relative URL, but structured data requires absolute
URLs for the image field to ensure proper portability across crawlers and
validators. Modify the jsonLd object to ensure the image property receives an
absolute URL by converting thumbnailUrl to a fully qualified URL (including the
domain) before assigning it to the image field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 875fa46f-6a17-4989-9b18-2d425c655e61

📥 Commits

Reviewing files that changed from the base of the PR and between cf6ef01 and 6044270.

⛔ Files ignored due to path filters (9)
  • docs/redesign/blog.png is excluded by !**/*.png
  • docs/redesign/contact.png is excluded by !**/*.png
  • docs/redesign/falsafah.png is excluded by !**/*.png
  • docs/redesign/forum.png is excluded by !**/*.png
  • docs/redesign/home.png is excluded by !**/*.png
  • docs/redesign/projects.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/brand/icon-192.png is excluded by !**/*.png
  • src/assets/brand/indopensource-hero.jpg is excluded by !**/*.jpg
📒 Files selected for processing (33)
  • .env.example
  • astro.config.mjs
  • package.json
  • scripts/sync-blog-posts.mjs
  • scripts/sync-projects.mjs
  • src/components/BaseButton.astro
  • src/components/HomeHero.astro
  • src/components/InfoCard.astro
  • src/components/LightSvgMotion.astro
  • src/components/PageHeader.astro
  • src/components/ProjectCard.astro
  • src/components/ProjectsDirectory.astro
  • src/components/RoadmapTimeline.astro
  • src/components/SectionHeader.astro
  • src/components/SiteFooter.astro
  • src/components/SiteHeader.astro
  • src/layouts/BaseLayout.astro
  • src/lib/content.ts
  • src/lib/hero.ts
  • src/lib/projects.ts
  • src/pages/blog.astro
  • src/pages/blog/[slug].astro
  • src/pages/contact.astro
  • src/pages/falsafah.astro
  • src/pages/forum.astro
  • src/pages/index.astro
  • src/pages/projects.astro
  • src/pages/projects/[slug].astro
  • src/pages/robots.txt.ts
  • src/pages/sitemap-index.xml.ts
  • src/pages/sitemap.xml.ts
  • src/styles/global.css
  • test/lib.test.mjs
💤 Files with no reviewable changes (1)
  • src/pages/sitemap-index.xml.ts

Comment thread scripts/sync-blog-posts.mjs
Comment on lines +250 to +263
// Only trust strict ISO 8601 dates (`YYYY-MM-DD`, optionally with a time) so
// ordering is deterministic across machines/locales. `Date.parse` is lenient
// and locale-dependent (e.g. it silently accepts "14 Juni 2026"), which would
// reintroduce the nondeterministic ordering CC-4 is about. Anything that is
// not strict ISO is treated as "no usable date" and pushed to the end.
if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) {
if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`);
return null;
}
const time = Date.parse(trimmed);
if (Number.isNaN(time)) {
console.warn(`Invalid blog date "${value}" — sorting this post last.`);
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Timezone-less datetimes still parse in local time, so ordering can differ across machines.

The validator allows datetime values without Z/offset. Date.parse() then uses local timezone semantics, which undermines the deterministic sort guarantee.

Proposed fix
   if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) {
     if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`);
     return null;
   }
+  const hasTime = /[T ]\d{2}:\d{2}/.test(trimmed);
+  const hasTimezone = /(Z|[+-]\d{2}:?\d{2})$/.test(trimmed);
+  if (hasTime && !hasTimezone) {
+    console.warn(`Timezone-less blog date "${value}" — sorting this post last (add Z or ±hh:mm).`);
+    return null;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Only trust strict ISO 8601 dates (`YYYY-MM-DD`, optionally with a time) so
// ordering is deterministic across machines/locales. `Date.parse` is lenient
// and locale-dependent (e.g. it silently accepts "14 Juni 2026"), which would
// reintroduce the nondeterministic ordering CC-4 is about. Anything that is
// not strict ISO is treated as "no usable date" and pushed to the end.
if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) {
if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`);
return null;
}
const time = Date.parse(trimmed);
if (Number.isNaN(time)) {
console.warn(`Invalid blog date "${value}" — sorting this post last.`);
return null;
}
// Only trust strict ISO 8601 dates (`YYYY-MM-DD`, optionally with a time) so
// ordering is deterministic across machines/locales. `Date.parse` is lenient
// and locale-dependent (e.g. it silently accepts "14 Juni 2026"), which would
// reintroduce the nondeterministic ordering CC-4 is about. Anything that is
// not strict ISO is treated as "no usable date" and pushed to the end.
if (!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(trimmed)) {
if (trimmed) console.warn(`Non-ISO blog date "${value}" — sorting this post last (use YYYY-MM-DD).`);
return null;
}
const hasTime = /[T ]\d{2}:\d{2}/.test(trimmed);
const hasTimezone = /(Z|[+-]\d{2}:?\d{2})$/.test(trimmed);
if (hasTime && !hasTimezone) {
console.warn(`Timezone-less blog date "${value}" — sorting this post last (add Z or ±hh:mm).`);
return null;
}
const time = Date.parse(trimmed);
if (Number.isNaN(time)) {
console.warn(`Invalid blog date "${value}" — sorting this post last.`);
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sync-blog-posts.mjs` around lines 250 - 263, The regex pattern in the
date validation check accepts timezone-less datetimes (e.g.,
"2023-06-14T14:30:00") which parse using local machine timezone, breaking
deterministic sorting. The timezone indicator at the end of the regex pattern is
currently optional due to the trailing question mark. Modify the regex to
require the timezone indicator (Z or UTC offset) whenever a time component is
present, or alternatively, restrict the validator to only accept plain dates in
YYYY-MM-DD format without time components. This ensures Date.parse always
interprets dates consistently across all machines regardless of local timezone
settings.

Comment on lines +63 to +65
navItems.map(([label, href]) => {
const isCurrent = currentPathWithoutBase === href;
return isCurrent ? (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use section-aware matching for active nav state.

At Line 64 and Line 99, strict equality means /blog/[slug]/ and /projects/[slug]/ won’t mark their parent nav item as current, so visual active state and aria-current are incorrect on detail pages.

Suggested patch
 const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replace(/\/$/, '')}`), '') || '/';
+const isCurrentPath = (href: string) =>
+  href === '/' ? currentPathWithoutBase === '/' : currentPathWithoutBase.startsWith(href);
@@
-          const isCurrent = currentPathWithoutBase === href;
+          const isCurrent = isCurrentPath(href);
@@
-      const isCurrent = currentPathWithoutBase === href;
+      const isCurrent = isCurrentPath(href);

Also applies to: 98-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/SiteHeader.astro` around lines 63 - 65, The isCurrent check at
lines 63-65 in SiteHeader.astro uses strict equality which fails to mark parent
nav items as current when viewing detail pages like /blog/my-post/ since they
don't exactly match /blog/. Additionally, the same issue exists at lines 98-100.
Replace the strict equality comparison with section-aware matching that checks
if the current path starts with the href value, ensuring parent navigation items
are correctly marked as current for both overview pages and their dynamic route
detail pages. Apply this fix to both locations where isCurrent is evaluated.

Comment thread src/pages/blog/[slug].astro
Comment thread src/pages/index.astro
Comment on lines 14 to 15
const totalProjects = projects.length;
const topProjects = projects.slice(0, 5);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter out failed sync entries before computing topProjects.

Line 15 slices from all projects, so homepage cards can still include syncFailed entries that are excluded elsewhere. This can surface partial/unavailable repos in the featured rail.

Suggested fix
 const totalProjects = projects.length;
-const topProjects = projects.slice(0, 5);
+const refreshedProjects = projects.filter((project) => !project.syncFailed);
+const topProjects = refreshedProjects.slice(0, 5);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalProjects = projects.length;
const topProjects = projects.slice(0, 5);
const totalProjects = projects.length;
const refreshedProjects = projects.filter((project) => !project.syncFailed);
const topProjects = refreshedProjects.slice(0, 5);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/index.astro` around lines 14 - 15, The topProjects variable is
being created by slicing directly from the full projects array, which includes
entries with syncFailed status. These failed sync entries should be excluded
from the featured rail on the homepage. Filter the projects array to exclude any
entries where syncFailed is true before slicing to create topProjects, ensuring
that only successfully synced repositories appear in the featured cards.

Comment thread src/styles/global.css

@import "tailwindcss";

@theme {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether stylelint config still rejects Tailwind v4 directives.
fd -HI 'stylelint\.config|\.stylelintrc' | while read -r f; do
  echo "===== $f ====="
  sed -n '1,220p' "$f"
done

echo "===== stylelint rule references ====="
rg -n "at-rule-no-unknown|scss/at-rule-no-unknown|tailwind|value-keyword-case|declaration-empty-line-before|property-no-deprecated"

Repository: IndopenSource/indopensource.org

Length of output: 7017


🏁 Script executed:

# Check the global.css file content around line 16
wc -l src/styles/global.css
echo "===== global.css content ====="
cat -n src/styles/global.css | head -30

Repository: IndopenSource/indopensource.org

Length of output: 1807


🏁 Script executed:

# Check for at-rule-no-unknown in the stylelint config and related documentation
rg -A 5 -B 5 "at-rule-no-unknown" .stylelintrc.json

Repository: IndopenSource/indopensource.org

Length of output: 57


🌐 Web query:

Tailwind CSS v4 @theme directive stylelint configuration

💡 Result:

To use Stylelint with Tailwind CSS v4 and the @theme directive, you must configure Stylelint to recognize Tailwind’s custom CSS at-rules and syntax, as standard CSS parsers will otherwise flag them as errors [1][2][3]. Since Tailwind CSS v4 utilizes the @theme directive to define design tokens [4], standard Stylelint configurations often require supplemental settings to prevent validation failures. The most effective approach is to use a dedicated Stylelint configuration package specifically built for Tailwind CSS v4+ [1][2][5]. Recommended Configuration Approach Several community-maintained packages, such as @dreamsicle.io/stylelint-config-tailwindcss, are designed to implement Tailwind's directives (including @theme) into Stylelint's languageOptions [1][5]. 1. Install the package: npm install --save-dev @dreamsicle.io/stylelint-config-tailwindcss 2. Add it to your stylelint.config.mjs (or equivalent configuration file): / * @type {import("stylelint").Config} */ export default { extends: [ "stylelint-config-standard", "@dreamsicle.io/stylelint-config-tailwindcss", ], }; Why this is necessary: Tailwind CSS v4 introduces custom syntax that extends beyond standard CSS specifications [4]. By using a specialized configuration, you ensure that Stylelint understands the @theme directive and associated custom properties as valid syntax rather than unknown at-rules [1][6]. These configurations typically inject Tailwind's custom at-rules, properties, and types into Stylelint’s syntax validation engine, allowing for proper linting without suppressing critical CSS errors [1][3]. If you are not using a pre-built configuration, you would otherwise need to manually configure Stylelint’s customSyntax or ignore the specific at-rule violations using the rules configuration (e.g., at-rule-no-unknown), which is generally discouraged as it sacrifices linting quality [1][2].

Citations:


Update stylelint config to accept Tailwind v4 @theme directive.

Line 16 uses a valid Tailwind v4 directive, but standard stylelint configurations will report it as an unknown at-rule. This will block CI merges despite the CSS being valid. Add a Tailwind-specific stylelint configuration (e.g., @dreamsicle.io/stylelint-config-tailwindcss) or manually configure the at-rule-no-unknown rule to allow Tailwind's custom at-rules.

🧰 Tools
🪛 Stylelint (17.12.0)

[error] 16-16: Unexpected unknown at-rule "@theme" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/global.css` at line 16, The stylelint configuration does not
recognize Tailwind v4's `@theme` directive, causing CI failures for valid CSS.
Update your stylelint configuration file (typically `.stylelintrc.js`,
`.stylelintrc.json`, or similar) to allow Tailwind's custom at-rules by either
installing and extending a Tailwind-specific stylelint config package like
`@dreamsicle.io/stylelint-config-tailwindcss`, or manually modify the
`at-rule-no-unknown` rule in your existing stylelint config to add an exceptions
list that includes Tailwind's custom at-rules (such as `theme`, `layer`,
`apply`, etc.).

Source: Linters/SAST tools

Comment thread src/styles/global.css
Comment on lines +178 to +186
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace deprecated .sr-only clipping rule.

Line 185 uses clip, which is deprecated and already flagged by stylelint. Prefer clip-path: inset(50%) (optionally keep clip as fallback only if legacy support is required).

Suggested patch
 .sr-only {
   position: absolute;
   width: 1px;
   height: 1px;
   padding: 0;
   margin: -1px;
   overflow: hidden;
-  clip: rect(0, 0, 0, 0);
+  clip-path: inset(50%);
+  clip: rect(0, 0, 0, 0); /* optional fallback */
   white-space: nowrap;
   border: 0;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
clip: rect(0, 0, 0, 0); /* optional fallback */
white-space: nowrap;
🧰 Tools
🪛 Stylelint (17.12.0)

[error] 185-185: Deprecated property "clip" (property-no-deprecated)

(property-no-deprecated)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/global.css` around lines 178 - 186, In the `.sr-only` class
definition, the deprecated `clip` property needs to be replaced. Remove or
replace the line containing `clip: rect(0, 0, 0, 0);` with the modern
`clip-path: inset(50%);` property. If legacy browser support is required, you
may keep the deprecated `clip` property as a fallback on a separate line before
the `clip-path` property, but the modern property should be the primary rule.

Source: Linters/SAST tools

… parser)

Address 4 findings from Codex/CodeRabbit review:

- BaseLayout: escape `<` in JSON-LD before emit so `</script>` inside
  synced repo descriptions / post titles can't break out of the inline
  block. Escaped before hashing, so the CSP SHA-256 still matches.
- blog/[slug]: allowlist author.url scheme (new safeHref) before it
  reaches href/JSON-LD, blocking javascript:/data: links.
- sync-projects: rebase root-relative README paths (/docs/x) onto the
  repo root instead of collapsing them to the host root (github.com/docs/x).
- sync-blog-posts: keep a lone `status:` an empty scalar instead of
  coercing it to [], which had corrupted publish gating.

Tests: +3 safeHref cases (19 pass); parser fixes verified against the
real script source. astro check clean, build = 60 pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants